import { WebSocket as ws, WebSocketServer } from "ws"
import { readFileSync } from 'fs';
import { createServer, Server } from "https"
import { IncomingMessage } from "http"
import { parse } from "path"
import { type Log4js, type Logger } from "log4js"

let log: Logger;
const QUALITY_OK: number = 0xc0;
const QUALITY_BAD: number = 0x00;
const QUALITY_NO_CONNECT: number = 0x8;

type TAG_TYPE_NAME = "string" | "number" | "boolean"
type TAG_TYPE = string | number | boolean;
type RPT = Pick<Tag, "name" | "type" | "value" | "quality" | "timeStamp" | "readonly">;
type PPT = Pick<RPT, "name" | "quality" | "timeStamp" | "value">
type WPT = { [key: string]: TAG_TYPE }
type Msg_Create = {
    cmd: "create",
    pt: RPT[];
}
type Msg_Regist = {
    cmd: "regist",
    pt: string[];
}
type Msg_Write = {
    cmd: "write",
    pt: WPT;
}
type Msg_Notify = {
    cmd: "notify",
    pt: PPT[];
}

type Message = Msg_Create | Msg_Regist | Msg_Write | Msg_Notify

class Tag {
    name: string;
    type: TAG_TYPE_NAME;
    value: TAG_TYPE;
    quality: number;
    timeStamp: number;
    producer: Producer | undefined;
    consumers: Set<Consumer>;
    readonly: boolean;
    constructor(producer: Producer | undefined, tag: RPT) {
        this.name = tag.name;
        this.type = tag.type
        this.value = tag.value;
        this.quality = tag.quality;
        this.timeStamp = tag.timeStamp;
        this.readonly = tag.readonly;
        this.producer = producer;
        this.consumers = new Set();
    }
}

class AddressSpace {
    private _tags: Map<string, Tag>;
    private _changes: Set<Tag>;
    private timer() {
        if (this._changes.size == 0) {
            return;
        }
        const info = new Map<Consumer, Set<Tag>>;
        this._changes.forEach(v => {
            v.consumers.forEach(c => {
                let ts = info.get(c)
                if (ts) {
                    ts.add(v);
                } else {
                    ts = new Set([v]);
                    info.set(c, ts);
                }
            })
        })
        info.forEach((v, k) => {
            k.notify(v);
        })
        this._changes.clear();
    }
    constructor() {
        this._changes = new Set();
        this._tags = new Map();
        setInterval(this.timer.bind(this), 20);
    }

    public regist_producer(producer: Producer) {
        log.trace("Registered Producer. IP:%s\t Port:%d", producer.ip, producer.port);
        producer.tags.forEach(tag => {
            let x = this._tags.get(tag.name);
            if (x) {
                log.warn(`"${tag.name}" tag already exists`)
                //如果已经存在Tag,复制消费者列表到新的tag
                log.warn("There are %d consumers, and consumers are transferred to new points.", x.consumers.size)
                x.consumers.forEach(i => {
                    tag.consumers.add(i)
                    i.tags.delete(x!);
                    i.tags.add(tag);
                })
                //从变更收集容器中删除，随后添加新的点
                this._changes.delete(x);
                this._changes.add(tag);
            }
            this._tags.set(tag.name, tag);
        })
    }
    public unregist_producer(producer: Producer) {
        //如果不存在消费者，则删除tag
        //如果同时存在收集到的变更，同时删除
        log.trace(`Producer disconnected. IP:${producer.ip}:${producer.port}`);
        producer.tags.forEach(v => {
            //确保producer能够自动销毁
            v.producer = undefined;
            v.quality = QUALITY_NO_CONNECT;
            //如果没有消费者，则删除对该tag的引用
            if (v.consumers.size == 0) {
                log.trace(`${v.name}No consumers,Delete tag.`)
                this._tags.delete(v.name);
                this._changes.delete(v);
            } else {
                this._changes.add(v);
            }
        })
    }

    public regist_consumer(consumer: Consumer, interests: string[]) {
        interests.forEach(tn => {
            let tag = this._tags.get(tn);
            if (tag) {
                tag.consumers.add(consumer);
                consumer.tags.add(tag);
            } else {
                const ctag = new Tag(undefined, { name: tn, type: "number", quality: QUALITY_NO_CONNECT, readonly: false, timeStamp: 0, value: 0 })
                ctag.consumers.add(consumer)
                consumer.tags.add(ctag)
                this._tags.set(tn, ctag)
            }
        })
    }
    public unregist_consumer(consumer: Consumer) {
        consumer.tags.forEach(tag => {
            tag.consumers.delete(consumer);
            if (!tag.producer) {
                this._changes.delete(tag);
                this._tags.delete(tag.name);
            }
        })
    }
    public consumer_write(tags: WPT) {
        let inf: Map<Producer, WPT> = new Map();
        for (let tn in tags) {
            let x = this._tags.get(tn);
            if (x && x.producer) {
                let obj = inf.get(x.producer);
                if (obj) {
                    obj[tn] = tags[tn]
                } else {
                    obj = {};
                    obj[tn] = tags[tn];
                    inf.set(x.producer, obj);
                };
            }
        }
        inf.forEach((v, k) => {

            k.write(v);
        })
    }
    public producer_write(producer: Producer, tags: PPT[]) {
        tags.forEach(tag => {
            let x = producer.tags.get(tag.name);
            if (x) {
                Object.assign(x, tag);
                this._changes.add(x);
            } else {
                log.warn(`Unknown tag:${tag.name}`);
            }
        })
    }
}
const addressSpace = new AddressSpace();

class Producer {
    ws: ws;
    tags: Map<string, Tag>;
    ip: string;
    port: number;
    private onClose() {
        addressSpace.unregist_producer(this);
    }
    private onMessage(msg: Message) {
        if (msg.cmd == "notify") {
            addressSpace.producer_write(this, msg.pt);
        } else {
            log.warn(`Unknown Message:"${msg.cmd}"`)
        }
    }
    constructor(ws: ws, tags: RPT[], addr: string, port: number) {
        this.ws = ws;
        this.tags = new Map();
        this.ip = addr;
        this.port = port;
        ws.on("close", this.onClose.bind(this));
        ws.on("message", e => this.onMessage(JSON.parse(e.toString("utf-8"))));
        tags.forEach(tag => {
            let x = new Tag(this, tag);
            this.tags.set(tag.name, x);
        })
        addressSpace.regist_producer(this);
    }
    public write(pts: WPT) {
        const str = JSON.stringify(pts);
        this.ws.send(str);
    }
}

class Consumer {
    private _ws: ws;
    tags: Set<Tag>;
    addr: string;
    port: number;
    timerPingPong?: NodeJS.Timeout;
    onMessage(msg: Message) {
        if (msg.cmd == "write") {
            addressSpace.consumer_write(msg.pt)
        } else {
            log.warn(`Unknown Message:"${msg.cmd}"`)
        }
        this.timerPingPong!.refresh();
    }
    send(msg: string) {
        this._ws.send(msg);
        this.timerPingPong!.refresh();
    }
    pingpong() {
        this._ws.ping();
    }
    constructor(ws: ws, interests: string[], addr: string, port: number) {
        this._ws = ws;
        this.addr = addr;
        this.port = port;
        this.tags = new Set();
        ws.onmessage = ev => this.onMessage(JSON.parse(ev.data.toString("utf-8")))
        ws.onclose = this.onDisconnect.bind(this)
        this.timerPingPong = setInterval(this.pingpong.bind(this), 60000)
        addressSpace.regist_consumer(this, interests);
        this.notify(this.tags);
    }
    onDisconnect() {
        clearInterval(this.timerPingPong);
        this.timerPingPong = undefined;
        this.tags.forEach(tag => {
            tag.consumers.delete(this);
        });
    }
    notify(tags: Set<Tag>) {
        const x = [...tags];
        const json = JSON.stringify(x, ["name", "quality", "timeStamp", "value"]);
        this.send(json);
    }
}
const path = parse(process.argv[1]);
class OPCServer {
    static _option = {
        cert: readFileSync("/etc/ssl/certs/mini-tiger.com_bundle.pem"),
        key: readFileSync("/etc/ssl/private/mini-tiger.com.key"),
        // key: readFileSync("../certificate/mini-tiger.com.key")
    }
    private _port: number;
    private _server: Server;
    private _wss: WebSocketServer;
    private connection(ws: ws, request: IncomingMessage) {
        const addr = request.socket.remoteAddress!;
        const port = request.socket.remotePort!;
        log.trace(`Receive a new connection:${request.socket.remoteAddress}:${request.socket.remotePort}`)
        //超时5秒，没有收到注册信息自动关闭连接
        const tvout = setTimeout(() => {
            log.warn("Registration message timeout.")
            ws.onmessage = null;
            ws.close();
        }, 5000);
        //接收注册消息
        ws.onmessage = function (ev) {
            this.onmessage = null;
            clearTimeout(tvout);
            let msg: Message = JSON.parse(ev.data.toString("utf-8"));
            switch (msg.cmd) {
                case "create":
                    log.trace("New producers.")
                    new Producer(this, msg.pt, addr, port);
                    break;
                case "regist":
                    log.trace("New consumers.")
                    new Consumer(this, msg.pt, addr, port);
                    break;
                default:
                    this.close();
            }
        }
    }
    private error(e: Error) {
        log.error("Server failure. Message:%s", e.message)
    }
    constructor(port: number) {
        this._server = createServer(OPCServer._option)
        this._port = port;
        this._wss = new WebSocketServer({ server: this._server ,perMessageDeflate:true});
        this._wss.on("connection", this.connection.bind(this));
        this._wss.on("error", this.error.bind(this));
    }
    public open() {
        this._server.listen(this._port);
    }
}
function startServer(log4j: Log4js) {
    log = log4j.getLogger("OPC")
    let server = new OPCServer(3000);
    server.open();
}
export {
    startServer
}